package Gtk2::Ex::FormManager;

use strict;
use Carp;
use Gtk2;

our $VERSION = '0.01';

=head1 NAME

Gtk2::Ex::FormManager - Autogenerate input forms

=head1 SYNOPSIS

	use Gtk2;
	use Gtk2::Ex::FormManager;

	my $fm = Gtk2::Ex::FormManager->new();
	
	$fm->add_fields(
		# name, label, type, default, test
		['name', 'Name',   'string', '',      1],
		['src',  'Source', 'file',   '',      1],
		['type', 'Type',   'string', 'Image', 0],
	);
	$fm->add_ui(q{
	<form name='NewObjectForm'>
		<label field='type'/>
		<input field='name'/>
		<input field='src'/>
	</form>
	});
	$fm->signal_connect(set_name => \&name_changed);
	
	my $widget = $fm->get_widget('NewObjectForm');
	my $values = $fm->get_hash(); # hash is tied!

=head1 DESCRIPTION

This module tries to provide a structured way to maintain hashes
with user preferences or state information in an application.

The tied hash is used to trigger the apropriate actions when a
setting is changed and can be used to enforce checking whether
a value is valid.

The second function of this module is to provide the gui parts
needed to present the user with a preferences dialog which allows
changing the values in this hash.

=head1 FIELDS

Fields are array references describing one input field.
The array contains:

=over 4

=item name

The name of this field. This is also the hash key to find the value
for this input.

=item label

Visible name of this field.

=item type

Type of the input field. Valid types are 'string', 'int', 'bool' and 'file'.

For multiple-choice options 'type' can be a array reference containing
possible value.

=item default

Default value for this input. The hash will be initialised with this value.

=item test

If 'test' is false any value is accepted.

If 'test' is true this input always needs to have a value. It is not allowed to
set it to empty string or C<undef>.

For type is 'string' you can specify a reference to a regex.

If the test is a code reference this code will be excuted and needs to return boolean.

=back

=head1 UI

The visual layout of forms construced by this manager is controlled by
one or more pieces of xml.

Elements used:

=over 4

=item form

Toplevel element to group a set of inputs.

=item input

Input field. Type of widget depends on the type of the field represented
by this element.

=item label

Displays the value of a field. This can regarded as a non-editable input.

TODO - this element is not yet implemented

=back

=head1 METHODS

=over 4

=item C<new()>

Simple constructor.

=cut

sub new {
	my $class = shift;
	my $self = bless {
		fields     => {}, # field specs
		ui         => {}, # ui structures
		listeners  => {}, # connected signals
	#	hash       => {}, # values
	}, $class;

	my %hash;
	tie %hash, 'Gtk2::Ex::FormManager::Hash', $self;
	$self->{hash} = \%hash;

	return $self;
}

=item C<signal_connect(SIGNAL, CODE, USERDATA)>

Add a handler fo a certain signal. To connect to a certain setting you
should connect to a signal that is the name of the setting prefixed by 'set_'.

=cut

sub signal_connect {
	my ($self, $signal, @handler) = @_;
	$self->{listeners}{$signal} ||= [];
	push @{$self->{listeners}{$signal}}, \@handler;
}

=item C<signal_emit(SIGNAL, ARGS, ..)>

Emits a signal.

=cut

sub signal_emit {
	my ($self, $signal, @args) = @_;
	return unless exists ${$self->{listeners}}{$signal}
			and ref $self->{listeners}{$signal};
	for (@{$self->{listeners}{$signal}}) {
		my ($code, @data) = @$_;
		$code->($self, @args, @data);
	}
}

=item C<add_fields(FIELD1, FIELD1, ..)>

Add a set input fields.
This method returns an id that can be used for C<remove_fields()>.

=cut

sub add_fields {
	my $self = shift;
	for (@_) {
		$self->{fields}{$$_[0]} = $_;
	}
	return 1;
}

=item C<remove_fields(ID)>

TODO

=cut

sub remove_fields { die 'TODO' }

=item C<add_ui(STRING)>

TODO

Add a xml layout.
This method returns an id that can be used for C<remove_ui()>.

=cut

sub add_ui {
	my ($self, $xml) = @_;

	die 'TODO';
	# Parse xml
	# FIXME

	return 1;
}

=item C<remove_ui(ID)>

TODO

=cut

sub remove_ui { die 'TODO' }

=item C<get_toplevels()>

Returns a list with toplevel ui elements.

=cut

sub get_toplevels {
	my $self = shift;
	die 'TODO';
	# FIXME
}

=item C<get_widget(TOPLEVEL)>

TODO

=cut

# How should widgets handle invalid input ??
# should they die or reset silently ?

sub get_widget {
	my ($self, $form) = @_;
	# FIXME translate ui structure in proxies
	die 'TODO';
}

=item C<get_hash()>

Returns a reference to the tire hash containing the actual values of the 
settings that are being managed.

=cut

sub get_hash { $_[0]->{hash} }

=item C<test(KEY => VALUE)>

Checks whether VALUE is ok for KEY to set in the hash.

=cut

sub test {
	my ($self, $key, $value) = @_;
	croak "Invalid key: $key" unless exists ${$$self{fields}}{$key};
	
	my ($type, $default, $test) = @{$$self{fields}{$key}}[2 .. 4];
	my $ok;
	if (ref $type) { $ok = grep {$_ eq $value} @$type }
	elsif ($type eq 'int' ) { $ok = ($value =~ /^(\d+)$/) }
	elsif ($type eq 'bool') { $ok = ( ($value == 1) || ($value == 0) ) }
	else { $ok = 1 } # no test for type 'string' and 'file'
	
	return $ok unless $ok and defined $test;
	
	if (ref $test) {
		if (ref($test) eq 'Regexp') { $ok = 1 if $value =~ $test   }
		else { $ok = $test->($value) } # CODE
	}
	elsif ($test == 1 and ! length $value) {
		$ok = defined $default;
	}
	else { $ok = 1 }
	
	return $ok;
}

=item C<set_sensitive(FIELD, 0|1)>

TODO

=cut

# needs a back reference to proxies
# add signal to proxies to make them disconnect properly

sub set_sensitive { die 'TODO' }

package Gtk2::Ex::FormManager::Hash;

use strict;
use Carp;

=back

=head1 HASH

The tied hash contains the actual values for all input fields.
This hash is tied and will throw an exception if you try to
store an invalid value into it. This hash is updated automaticly
by the widgets that are generated by the Form Manager.

=cut



sub TIEHASH {
	my ($class, $manager) = @_;
	bless [{}, $manager], $class;
}

sub FETCH {
	my ($hash, $manager) = @{ shift() };
	my $key = shift;
	croak "Invalid key: $key" unless exists ${$$manager{fields}}{$key};
	my $value = $$hash{$key};
	my $default = $$manager{fields}{$key}[3];
	return defined($value) ? $value : $default ;
}

sub STORE {
	my ($hash, $manager) = @{ shift() };
	my ($key, $value) = @_;
	croak "Invalid key: $key" unless exists ${$$manager{fields}}{$key};
	croak "Invalid value: $key => $value" unless $manager->test($key => $value);
	$$hash{$key} = $value;
	$manager->signal_emit('set_'.$key => $value);
}

sub DELETE { goto \&STORE } # $value will be undef

sub CLEAR { croak 'Can\'t clear this hash' }

sub EXISTS { exists $_[0]->[1]{$_[1]} }

sub FIRSTKEY {
	my ($hash, $manager) = @{ shift() };
	keys %{$manager->{fields}};
	each %{$manager->{fields}};
}

sub NEXTKEY {
	my ($hash, $manager) = @{ shift() };
	each %{$manager->{fields}};
}

1;

__END__

=head1 AUTHOR

Jaap Karssenberg (Pardus) E<lt>pardus@cpan.orgE<gt>

Copyright (c) 2006 Jaap G Karssenberg. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 SEE ALSO

=cut

